feat(smart-sessions): experimental spend-session abstraction + settlement-layer policy encoding (RHI-6242) - #807
feat(smart-sessions): experimental spend-session abstraction + settlement-layer policy encoding (RHI-6242)#807highskore wants to merge 5 commits into
Conversation
Declare a spend in business terms — tokens, amounts, recipients, target
chains, settlement layers — and let the SDK formulate the session-policy
combination for the appropriate route:
- same-chain → executor route: universal-action / arg-policy scoping on
the ERC-20 transfer (recipient allowlist + amount cap) + time-frame.
- cross-chain → permit2 route: a claim policy bound to the settlement
layer's arbiter, via the existing cross-chain permit expansion.
`singleUse` adds the one-time-use policy and yields the burn op; omitting
it is the explicit ("old way") config. Exposed as
experimental_defineSpendSession from smart-sessions, with unit coverage and
a local-fork demo scenario.
Closes RHI-6242.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…-session Add a per-layer capability table (LAYER_CAPABILITIES) and widen the spend target's settlementLayers to the full settlement-layer set. defineSpendSession now refuses — rather than silently emitting a restricted-looking but unrestricted session — when a requested layer is not enforceable yet (the IntentExecutor-backed layers, pending the settlement-layer adapter policy) or cannot bind a requested recipient/amount restriction. Arbiter layers (SAME_CHAIN/ECO/ACROSS) stay enforceable today via the Permit2 claim policy. Refs RHI-6242. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… + install path Add a faithful SDK encoder for the IntentExecutor settlement-layer policy family (smart-sessions-v2 #46): the shared base header, ownable multi-layer and static single-layer initData, and the CCTP/Relay/Rhino adapter config blobs — byte-packed to match the on-chain layout exactly (all scalars big-endian and unpadded; addresses raw 20 bytes; CCTP mint recipients raw 32 bytes). Add an `erc1271Policies` passthrough on SessionDefinition (resolve installs them on the 1271 AND-list and drops the default sudo entry so the list stays strict) plus an `intentExecutorPolicy` address slot, and wire defineSpendSession to accept a pre-encoded settlement-layer policy — guarded against combining it with the mutually-exclusive Permit2 claim policy. No canonical deployment exists yet, so the address is required (like oneTimeUseId) and the encoding is validated by byte-exact unit tests. Refs RHI-6242. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…traction Consensus fixes from three fresh-context reviews: - Refuse a fully-unrestricted same-chain spend (no recipients / maxAmount / time-window / singleUse) instead of silently emitting a sudo transfer allowance — the exact "restricted-looking but unrestricted" outcome the builder exists to prevent. - Require an explicit destination token per target chain (token addresses differ per chain — no source-address reuse) and refuse an empty `recipients: []` (was fail-open on the cross-chain route). - Remove the incoherent `route` override (session shape and burn-op route could diverge) and the dead, never-read `intentExecutorPolicy` address field. - Add token/chain enforceability refusals + width guards on the encoder inputs; freeze LAYER_CAPABILITIES; use viem `zeroAddress`. - Tighten tests: real per-chain dest tokens, a pinned (non-tautological) layer-id vector, and added empty-recipients / unrestricted-spend / missing-dest-token cases. biome clean. Refs RHI-6242. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
| encodeOneTimeUseIdInitData, | ||
| oneTimeUseIdErc1271Policy, | ||
| // IntentExecutor settlement-layer policy (experimental — no deployment yet) | ||
| addressToBytes32, |
There was a problem hiding this comment.
design-smell — AGENTS.md says every fix or feature needs a changeset, and public export additions require a minor changeset; this adds new @rhinestone/sdk/smart-sessions exports without any .changeset/*.md. The release would publish the new API without the required versioned changelog entry.
| // Pre-encoded ERC-1271 policy entries to install on the session's 1271 surface | ||
| // (e.g. an IntentExecutor settlement-layer policy). Enforcing 1271 policies drop | ||
| // the default sudo entry, so the 1271 list stays a strict AND. | ||
| erc1271Policies?: ResolvedPolicy[] |
There was a problem hiding this comment.
design-smell — This adds the raw 1271 policy field only to the internal domain type; the exported SessionDefinition in src/config/account.ts still lacks erc1271Policies, even though toSession() is typed against that public interface. Consumers can import intentExecutorPolicyEntry() but cannot pass its result to toSession({ erc1271Policies: ... }) without a cast.
| process.env.INTEGRATION_ARB_FORK_RPC ?? 'http://localhost:30002' | ||
| const API_KEY = process.env.INTEGRATION_RHINESTONE_API_KEY ?? 'testuserapikey' | ||
| const POLICY = (process.env.INTEGRATION_ONE_TIME_USE_ID_POLICY ?? '') as Address | ||
| if (!POLICY) { |
There was a problem hiding this comment.
design-smell — This import-time throw makes bun run test:integration fail during file discovery whenever INTEGRATION_ONE_TIME_USE_ID_POLICY is absent. Existing integration env checks happen inside the scenarios that need them, so this demo should skip or fail inside its own tests instead of aborting the whole suite at module load.
- Add the required changeset (minor) for the new experimental exports. - Surface `erc1271Policies` on the public SessionDefinition so a toSession caller can pass an intentExecutorPolicyEntry result without a cast. - Skip the spend-session demo itest (describe.skipIf) when the policy env var is absent, instead of throwing at import and aborting integration discovery. Refs RHI-6242. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
What
Experimental SDK abstraction for settlement-layer session policies (RHI-6242): declare a spend in business terms — tokens, amounts, recipients, target chains, settlement layers — and let the SDK formulate the underlying session-policy combination, instead of hand-wiring arbiter policies per layer.
experimental_defineSpendSession(input) → { session, route, buildBurnOp }:transfer(recipient allowlist + amount cap) + time-frame.singleUseinstalls the one-time-use policy and yields the burn op; omitting it is the explicit ("old way") config.Capability table + honest refusal
A per-layer capability table (
LAYER_CAPABILITIES) records, for each settlement layer, which of {recipient, amount, token, chain} it can actually enforce and whether it's available today. The builder refuses — rather than emitting a restricted-looking but unrestricted session — when a requested layer isn't enforceable yet (the IntentExecutor-backed layers) or can't bind a requested recipient/amount restriction.IntentExecutor settlement-layer policy encoding
Faithful byte-packed encoders for the IntentExecutor settlement-layer policy family (smart-sessions-v2 #46): the shared base header, ownable multi-layer and static single-layer initData, and the CCTP / Relay / Rhino adapter config blobs — matching the on-chain decode exactly (big-endian, unpadded; raw 20-byte addresses; raw 32-byte CCTP mint recipients). Installed via a new
erc1271Policiespassthrough onSessionDefinition(resolve drops the default sudo entry so the 1271 list stays a strict AND), guarded against combining with the mutually-exclusive Permit2 claim policy.The policy has no canonical deployment yet, so its address is required (like
oneTimeUseId) and the encoding is validated by byte-exact unit tests rather than e2e.Why arg-policy can't cover the IntentExecutor layers
For the IntentExecutor-backed layers (Rhino/Relay/CCTP/NEAR/OFT) the bridge deposit is decoded per-execution, so a single call's args are at fixed offsets — CCTP is fully pinnable by arg-policy, Rhino/NEAR partially. But the robust, all-layers answer is the decode-level IntentExecutor policy (adapters), because: the wildcard fallback action would let a settler route around per-action pins; Relay's calldata is opaque; and Rhino-native/NEAR have no on-chain recipient. Details in the RHI-6242 discussion.
Testing
spend-session.test.ts(route selection, scoping, refusal, one-time-use, erc1271 wiring) +settlement-layer.test.ts(byte-exact encoder field placement + size checklist). All green;tsc --noEmitclean.test/integration/scenarios/spend-session.demo.itest.ts— local-fork demo (same-chain settle-once/reject-twice + cross-chain build). On-chain settlement is currently gated by a separate settlement-layer signature issue, tracked independently; the abstraction's formulation is proven by the unit tests.Notes
experimental_prefix).feat/rhi-5798-sdk-onetime. Merge feat(smart-sessions): OneTimeUseId session wiring + burn-op helpers (RHI-5798) #792 first.Closes RHI-6242
🤖 Generated with Claude Code